Validation patterns

Some questions about data verification

Created by Ignasi 'Iggy' Bosch / @ignasibosch

  1. Introduction
  2. WHY
  3. WHAT
  4. WHERE
  5. WHO
  6. WHEN
  7. HOW

WHY

WHAT

WHERE



   

WHO

DATA

$this->validate($request, [
    'title' => 'required|unique:posts|max:255',
    'body' => 'required',
]);
                    
OBJECT

$author = new Author();
// ... whatever
$validator = $this->get('validator');
$errors = $validator->validate($author);
                    

class Author
{
    /**
     * @Assert\NotBlank()
     */
    public $name;
}
                    
ACTION

public $validate = array(
    'id' => array(
        'rule' => 'blank',
        'on' => 'create' <-------------
    )
);
                    
CONTEXT

try {
    $validator = ValidatorFactory::make(
                                        'checkOutOrder',
                                        Auth::role()
                                       );
    $order = $this->orderService
                    ->setData($post)
                    ->setValidator($validator)
                    ->checkOut();

} catch (ValidationException $ex) {
    return $this->returnBackWithErrors($ex);
}
                    

WHEN

HOW

HTML5


input:valid { border-color: green; }
input:invalid { border-color: red; }
                  


                    


                    


                    

public function check($date, $numberOfSeats)
{
    if ($date == ''){
        throw new IllegalArgumentException("date is missing");
    }
    if ($this->isBefore($date)){
        throw new IllegalArgumentException(
                        "date cannot be before today");
    }
    if ($numberOfSeats == ''){
        throw new IllegalArgumentException(
                        "number of seats cannot be empty");
    }
    if ($numberOfSeats < 1){
        throw new IllegalArgumentException(
                        "number of seats must be positive");
    }
}
                    

Exceptions vs Notifications


public function check($date, $numberOfSeats)
{
    $errors = []
    if ($date == ''){
        $errors[] = "date is missing";
    }
    if ($this->isBefore($date)){
        $errors[] = "date cannot be before today";
    }
    if ($numberOfSeats == ''){
        $errors[] = "number of seats cannot be empty";
    }
    if ($numberOfSeats < 1){
        $errors[] = "number of seats must be positive";
    }
    return $errors;
}

public function saveBooking($post)
{
    $errors = $this->check($post['date'], $post['number_seats']);
    if(!empty($errors)){
        throw new FormValidationException($errors);
    }
    $booking = new Booking();
    // ...
}
                    

class Validator
{
    protected $errors = [];

    public function checkRequired($value, $field)
    {
        if (!trim($value)) {
            $this->errors[$field] = 'Required value';
        }
        return $this;
    }

    public function checkAlphanumeric($value)
    {
        if ($value && !preg_match("/^[a-zA-Z0-9]+$/", $value)) {
            $this->errors[$field] =
                'The supplied value must be alphanumeric';
        }
        return $this;
    }

    public function isValid(){ return empty($this->errors);}

    public function getErrors(){ return $this->errors;}
}

    

$validator = new Validator();
$validator->checkRequired($post['name'], 'name')
            ->checkAlphanumeric($post['name'], 'name');

if (!$validator->isValid()) {
    return new JsonResponse($validator->getErrors(), 400);
}

/** Some action **/

                    

Strategy Pattern


class Required implements Rule
{
    public function isValid($value){ /****/ }
    public function getMessage(){ /****/ }
    private function checkSomething($value){ /****/ }
}

class Email implements Rule
{
    public function isValid($value){ /****/ }
    public function getMessage(){ /****/ }
    private function checkSomething($value){ /****/ }
}

class Alphanum implements Rule
{
    public function isValid($value){ /****/ }
    public function getMessage(){ /****/ }
    private function checkSomething($value){ /****/ }
}
                    

class UpdateEntryValidator extends AbstractValidator
                                            implements Validator
{
    public function __construct()
    {
        $this->rules = [
          'name' => [ new Required(), new Alphanum() ],
          'email' => [ new Required(), new Email() ]
        ];
    }
}
                    

abstract class AbstractValidator
{
    protected $rules = [];
    protected $errors = [];

    public function validate($post)
    {
        foreach ($post as $field => $value) {
            if(isset($this->rules[$field])){
                $this->checkRules($field, $value);
            }
        }
    }

    protected function checkRules($field, $value)
    {
        foreach ($this->rules[$field] as $rule) {
            if (!$rule->isValid($value)) {
                $this->errors[$field] = $rule->getMessage();
            }
        }
    }

    public function isValid(){ return empty($this->errors); }

    public function getErrors(){ return $this->errors; }
}
                    

     $action = Action::UPDATE;
     

$validator = EntryValidatorFactory::make($action);
$validator->validate($post);

if(!$validator->isValid()){
    return new JsonResponse($validator->getErrors(), 400);
}

/** Some action **/
                    

Event validation


class EntryCreatedEvent extends Event implements EntryEvent
{
    const NAME = 'entry.created';
    protected $entry;

    public function __construct(Entry $entry)
    {
        $this->entry = $entry;
    }

    public function getEntry()
    {
        return $this->entry;
    }
}
                    

class EntryValidationListener
{
    private $entry;
    private $validator;

    public function __construct(Validator $validator)
    {
        $this->validator = $validator;
    }

    public function onCreateAction(EntryEvent $event)
    {
        $this->validateEntry($event->getEntry());
    }

    private function validateEntry(Entry $entry)
    {
        $this->validator->setEntry($entry)->validate();

        if(!$this->validator->isValid()){
           throw new ValidationException(
                    $this->validator->getErrors());
        }
    }
}
                    

$dispatcher = new EventDispatcher();
$validator = new CreateEntryValidator();
$listener = new EntryValidationListener($validator);
$dispatcher->addListener(EntryCreatedEvent::NAME,
                        array($listener, 'onCreateAction'));
                    
WEB

$entry = new Entry();
// ... do something
$event = new EntryCreatedEvent($entry);

try {
    $dispatcher->dispatch(EntryCreatedEvent::NAME, $event);
} catch (ValidationException $e) {
    /** Return something to web **/
}

                    
CLI

$entry = new Entry();
// ... do something
$event = new EntryCreatedEvent($entry);

try {
    $dispatcher->dispatch(EntryCreatedEvent::NAME, $event);
} catch (ValidationException $e) {
    /** Return something to CLI **/
}

                    

Chain of Responsibility


class ShippedStatusValidator implements StatusValidatorInterface
{
    private $status;
    private $order
    private $isShippedStatus = false;

    public function __construct(Order $order)
    {
        $this->status = Status::SHIPPED;
        $this->order = $order;
    }

    public function process()
    {
        $this->validateData()
            ->hasShipmentDate()
            ->hasNotDeliveryDate()
            ->isNotLaterStatus();
        return $this->isShippedStatus ? $this->status : false;
    }

    protected function createdByAdmin(){ //... }
    protected function validateData(){ //... }
    protected function hasShipmentDate(){ //... }
    protected function hasNotDeliveryDate(){ //... }
    protected function isNotLaterStatus(){ //... }
}
                    

class StandardOrderStatusProcessor extends AbstractStatusProcessor
                        implements StatusProcessorInterface
{
    public function __construct(Order $order)
    {
        $validators = [
            new ProcessingStatusValidator($order),
            new ProcessAlertStatusValidator($order),
            new PaidStatusValidator($order),
            new CancelledStatusValidator($order),
            new PaymentAlertStatusValidator($order),
            new ExpiredStatusValidator($order),
            new ReadyAlertStatusValidator($order),
            new ShippedStatusValidator($order),
            new ShipmentAlertStatusValidator($order),
            new DeliveredStatusValidator($order),
            new RejectedStatusValidator($order),
            new CompleteStatusValidator($order)
        ];

        $this->setValidators($validators);
    }

}
                    

abstract class AbstractStatusProcessor
{
    private $validators = [];

    public function setValidators(array $validators)
    {
        foreach ($validators as $validator) {
            $this->addValidator($validator);
        }
    }

    public function addValidator
                        (StatusValidatorInterface $validator)
    {
        $this->validators[] = $validator;
    }

    public function getStatus()
    {
        for ($i = 0, $status = FALSE;
                        $i < count($this->validators)
                        && $status == FALSE;
                        $i++) {
            $status = $this->validators[$i]->process();
        }
        return $status ? $status : $this->throwsStatusException();
    }
}
                    

public function isValidStatus(Order $order)
{
  $statusProcessor = StatusProcessorFactory::make($order);

  try {

    return $order->getStatus() === $statusProcessor->getStatus();

  } catch (StatusException $ex) {

    return false;

  }
}
                    

Validation job queue


class ValidateEntries extends Job
                            implements SelfHandling, ShouldQueue
{
    use InteractsWithQueue, SerializesModels;

    public function handle()
    {
        $entries = Entry::where('status', Entry::STATUS_COMPLETE)
                    ->get();
        foreach($entries as $entry){
            $newStatus = $this->isValidEntry($entry)
                    ? Entry::STATUS_VALID
                    : Entry::STATUS_ERROR
            $this->updateEntry($entry, $newStatus);
        }
    }

    private function isValidEntry(Entry $entry)
    { /****/ }

    private function updateEntry(Entry $entry, $newStatus)
    { /****/ }
}
                
some Cool stuff:

SourceMaking: Chain of Responsibility
Martin Fowler: Replacing Throwing Exceptions with Notification
Martin Fowler: ContextualValidation
DevShed: Using Multiple Strategy Classes with the Strategy Design Pattern
Implementing Fowler's Analysis Validator Pattern in Java
JSR 303: Bean Validation
Web Form Validation: Best Practices and Tutorials
Code Validation and Exception Handling: From the UI to the Backend
Symfony Validation
Laravel Validation
CakePHP Validation

Thank You

https://ignasibosch.com/talks/validation-patterns

me@ignasibosch.com | @ignasibosch